﻿using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace DiscreteMath.Tests
{
    [TestClass]
    public class Automata
    {
        private TestableAutomaton[] automata;
        private IList<char> alphabetList;
        private WordGenerator generator;
        private EquivalenceTester tester;
        private Random rng;

        [TestInitialize]
        public void PrepareTests()
        {
            alphabetList = AutomataFactory.Factory.Alphabet.ToList();
            automata = AutomataFactory.Factory.GetAllTests().Cast<TestableAutomaton>().ToArray();
            generator = new WordGenerator(alphabetList);
            tester = new EquivalenceTester(TestConfigurationVariables.TestCount, generator);
            rng = new Random();
        }

        [TestMethod]
        public void Basic()
        {
            foreach (TestableAutomaton automaton in automata)
            {
                tester.TestEquivalence(automaton.Device.ReadWord, automaton.RecognitionTest, automaton.Name, Assert.AreEqual);
            }
        }

        [TestMethod]
        public void Union()
        {
            for (int i = 0; i < automata.Length; i++)
            {
                for (int j = i + 1; j < automata.Length; j++)
                {
                    var union = automata[i].Device + automata[j].Device;
                    Func<string, bool> unionTest = 
                        PredicateCombiner.CombineUnion(automata[i].RecognitionTest, automata[j].RecognitionTest);

                    tester.TestEquivalence(
                        union.ReadWord,
                        unionTest,
                        string.Format("Union of {0} and {1}", automata[i].Name, automata[j].Name),
                        Assert.AreEqual);
                }
            }
        }

        [TestMethod]
        public void Intersection()
        {
            for (int i = 0; i < automata.Length; i++)
            {
                for (int j = i + 1; j < automata.Length; j++)
                {
                    var machine1 = automata[i].Device as DeterministicAutomaton ?? automata[i].Device.Determine();
                    var machine2 = automata[j].Device as DeterministicAutomaton ?? automata[j].Device.Determine();
                    NondeterministicAutomaton intersection = machine1.Intersection(machine2);
                    Func<string, bool> intersectionTest = 
                        PredicateCombiner.CombineIntersection(automata[i].RecognitionTest, automata[j].RecognitionTest);

                    tester.TestEquivalence(intersection.ReadWord,
                        intersectionTest,
                        string.Format("Intersection of {0} and {1}", automata[i].Name, automata[j].Name),
                        Assert.AreEqual);
                }
            }
        }

        [TestMethod]
        public void Negation()
        {
            foreach (TestableAutomaton automaton in automata)
            {
                var machine = automaton.Device as DeterministicAutomaton ?? automaton.Device.Determine();
                var negated = -machine;
                tester.TestInequality(machine, negated, automaton.Name);
                tester.TestEquivalence(machine, -negated, automaton.Name);
            }
        }

        [TestMethod]
        public void KleneeStar()
        {
            NondeterministicAutomaton klenee = ~AutomataFactory.Factory.AB.Device;
            Func<string, bool> test = (word) =>
                {
                    if (word.Equals(SpecialSigns.EmptyWord.ToString()))
                        return true;

                    if (word.Length % 2 == 1)
                        return false;

                    for (int i = 0; i < word.Length; i += 2)
                    {
                        if (word.Substring(i, 2) != "ab")
                            return false;
                    }
                    return true;
                };

            tester.TestEquivalence(klenee.ReadWord, test, "Klenee of " + AutomataFactory.Factory.AB.Name, Assert.AreEqual);
            klenee = ~AutomataFactory.Factory.AAunionBB.Device;
            test = (word) =>
                {
                    if (word.Equals(SpecialSigns.EmptyWord.ToString()))
                        return true;

                    if (word.Length < 2 || word.Length % 2 == 1)
                        return false;

                    for (int i = 0; i < word.Length - 1; i += 2)
                    {
                        if (word[i] != word[i + 1])
                            return false;
                    }
                    return true;
                };
            tester.TestEquivalence(klenee.ReadWord, test, "Klenee of " + AutomataFactory.Factory.AAunionBB.Name, Assert.AreEqual);
            Assert.IsTrue(klenee.ReadWord(SpecialSigns.EmptyWord.ToString()), "Kleneed does not accept empty word");
        }

        private static List<int[]> GeneratePermutations(int elementsNumber, int sum)
        {
            int k = elementsNumber;
            int n = sum;
            List<int[]> permutations = new List<int[]>();
            if (k == 1)
            {
                return new List<int[]>() { new int[] { n } };
            }
            for (int i = 1; n - i > 0; i++)
            {
                int[] current = new int[k];
                List<int[]> subpermutations = GeneratePermutations(k - 1, n - i);
                foreach (int[] permutation in subpermutations)
                {
                    var next = new int[k];
                    next[0] = i;

                    for (int j = 1; j < k; j++)
                    {
                        next[j] = permutation[j - 1];
                    }
                    permutations.Add(next);
                }
            }

            return permutations;
        }

        [TestMethod]
        public void Concatenation()
        {
            for (int i = 0; i < automata.Length; i++)
            {
                for (int j = 0; j < automata.Length; j++)
                {
                    if (i == j)
                        continue;

                    var concatenated = automata[i].Device * automata[j].Device;
                    Func<string, bool> concatenationTest =
                        PredicateCombiner.CombineConcatenation(automata[i].RecognitionTest, automata[j].RecognitionTest);

                    tester.TestEquivalence(
                        concatenated.ReadWord,
                        concatenationTest,
                        string.Format("Concatenation of {0} and {1}", automata[i].Name, automata[j].Name),
                        Assert.AreEqual);
                }
            }
        }

        [TestMethod]
        public void Determinization()
        {
            foreach (TestableAutomaton test in automata.Where(x => !(x.Device is DeterministicAutomaton)))
            {
                var determined = test.Device.Determine();
                tester.TestEquivalence(test.Device, determined, test.Name);
                Assert.AreEqual(test.DeterminedNumberOfStates, determined.States.Count,
                    "Determined number of states in " + test.Name);
            }
        }

        [TestMethod]
        public void Minimization()
        {
            foreach (TestableAutomaton test in automata)
            {
                var machine = test.Device as DeterministicAutomaton ?? test.Device.Determine();
                var minimized = machine.Minimize();
                tester.TestEquivalence(machine, minimized, test.Name);
                Assert.AreEqual(test.MinimizedNumberOfStates, minimized.States.Count);
            }
        }

        [TestMethod]
        public void FromRegex()
        {
            foreach (TestableAutomaton test in automata)
            {
                NondeterministicAutomaton fromRegex = NondeterministicAutomaton.FromRegex(test.Regex);
                tester.TestEquivalence(test.Device, fromRegex, "From Regex of " + test.Name);
            }
        }

        [TestMethod]
        public void ToRegex()
        {
            foreach (TestableAutomaton test in automata)
            {
                var machine = NondeterministicAutomaton.FromRegex(test.Device.ToRegex());
                tester.TestEquivalence(test.Device, machine, "From -> To Regex of " + test.Name);
            }
        }

        [TestMethod]
        public void LanguageEquals()
        {
            foreach (TestableAutomaton firstTest in automata)
            {
                foreach (TestableAutomaton secondTest in automata)
                {
                    var determined1 = firstTest.Device.Determine();
                    var determined2 = secondTest.Device.Determine();

                    Assert.AreEqual(
                        determined1.LanguageEquals(determined2),
                        firstTest == secondTest,
                        string.Format("Language Equals of {0} and {1}", firstTest.Name, secondTest.Name));
                }
            }
        }

		[TestMethod]
		public void EquivalenceUnderJsonSerialization()
		{
			foreach (TestableAutomaton machine in this.automata)
			{
				NondeterministicAutomaton deserialized = JsonConvert.DeserializeObject<NondeterministicAutomaton>(JsonConvert.SerializeObject(machine.Device));
				tester.TestEquivalence(machine.Device, deserialized, machine.Name);
				tester.TestEquivalence(machine.Device.Determine(), deserialized.Determine(), machine.Name);
				tester.TestEquivalence(machine.Device.Determine().Minimize(), deserialized.Determine().Minimize(), machine.Name);
				tester.TestEquivalence(machine.Device.KleneeStar(), deserialized.KleneeStar(), machine.Name);
				tester.TestEquivalence(machine.Device.Determine().Negate(), deserialized.Determine().Negate(), machine.Name);
				
				NondeterministicAutomaton randomMachine = this.automata[rng.Next(this.automata.Length)];
				tester.TestEquivalence(machine.Device.Concatenate(randomMachine), deserialized.Concatenate(randomMachine), machine.Name);
				tester.TestEquivalence(machine.Device.Union(randomMachine), deserialized.Union(randomMachine), machine.Name);
			}
		}
    }
}
